import datetime, decimal, struct

def dbf_type_desc_for_code(code):
    descriptions = {'C':'character', 'N':'number', 'L':'boolean', 'D':'date', 'M':'memo',
                    'F':'float', 'B':'binary', 'G':'general', 'P':'picture', 'Y':'currency',
                    'T':'datetime', 'I':'int', 'V':'varifield', 'X':'variant', '@':'timestamp', 
                    'O':'double', '+':'autoincrement'}
    if code in descriptions:
        return descriptions[code]
    else:
        return 'unknown'

class FieldDescriptor(object):
    """Represents a field descriptor for a dBase table.
    The field descriptor attributes are:
    name -- a 10-character nul-terminated string
    type -- a single-letter code describing data type
    length -- length of field record in bytes
    numdecimal -- number of decimals for numeric formats
    displacement -- unused
    flags -- unused
    mdx_flag -- unused"""
    
    def __init__(self, record):
        """Initializes a field descriptor with 32-byte record from the dBase file."""
        fieldname, = struct.unpack('11s', record[0:11])
        # grab part of string before nul terminator
        self.name = fieldname[:fieldname.index('\x00')]
        self.type, self.displacement, self.length = struct.unpack('<ciB', record[11:17])
        self.numdecimal, self.flags = struct.unpack('<BB', record[17:19])
        # ignore incnext and incstep
        #self.incnext, self.incstep = struct.unpack('<iB', record[19:24])
        # 24--30 is reserved
        self.mdx_flag = bool(struct.unpack('<B', record[30:31])[0])

        # I think that displacement, flags, incnext, and incstep are all
        # for some other version of dbase than dbase iv.  there seem to
        # be conflicting specifications.

    def print_info(self):
        print 'name:', self.name
        print 'type:', self.type
        print 'displacement:', self.displacement
        print 'length:', self.length
        print 'numdecimal:', self.numdecimal
        print 'flags:', self.flags
        #print 'increments:', self.incnext, self.incstep

    def __repr__(self):
        return "FieldDescriptor<%s: %s; disp=%d; len=%d; numdecimal=%d; flags=%s>" % (self.name, dbf_type_desc_for_code(self.type), self.displacement, self.length, self.numdecimal, self.flags)
        

class dBaseTable(object):
    def __init__(self, filename):
        self.filename = filename

        with open(filename, 'rb') as f:
            self.read_file_header(f)
            self.read_field_descriptors(f)
            self.read_table_rows(f)

    def print_info(self):
        print 'last update:', self.last_update
        print 'num records:', self.num_records
        print 'header length:', self.header_length
        print 'record length:', self.record_length
        print 'table_flags:', self.table_flags
        print 'code_page_mark:', self.code_page_mark
        
    def read_file_header(self, f):
        version, = struct.unpack('<B', f.read(1))
        yy,mm,dd = struct.unpack('<BBB', f.read(3))
        self.last_update = datetime.date(yy+1900, mm, dd).isoformat()
        self.num_records, = struct.unpack('<I', f.read(4))
        self.header_length, = struct.unpack('<H', f.read(2))
        self.record_length, = struct.unpack('<H', f.read(2))
        f.read(16) # reserved
        self.table_flags, = struct.unpack('B', f.read(1))        
        self.code_page_mark, = struct.unpack('B', f.read(1))
        f.read(2) # reserved and filled with zeros

    def read_field_descriptors(self, f):
        """Read in the field descriptor data until '\x0d' is reached."""
        self.field_descriptors = []
        while True:
            firstbyte = f.read(1)
            if firstbyte == '\x0d':
                break
            field_descr = FieldDescriptor(firstbyte + f.read(31))
            self.field_descriptors.append(field_descr)

        # create a field name to column index table.
        self.field_index = {}
        index = 0
        for fd in self.field_descriptors:
            self.field_index[fd.name] = index
            index += 1

    def read_table_rows(self, f):
        """Reads in each record, storing data into table_rows."""
        self.table_rows = []
        for i in range(self.num_records):
            self.table_rows.append(self.read_record(f))

    def read_record(self, f):
        """Read in the data from a single record/table row."""
        record = f.read(self.record_length)
        # I'm ignoring the delete_flag at the moment, but I guess we could
        # simply not create a record for it?  Would this mess up the shapefile
        # object indexing?
        delete_flag = record[0:1]
        pos = 1  # byte position in the record
        fields = []
        for fd in self.field_descriptors:
            # Grab the bytes for the field.
            field = record[pos:pos + fd.length]
            
            # Then convert it to appropriate python type based on typecode.
            if fd.type == 'N':   # numeric
                if fd.numdecimal == 0:
                    field = int(field)
                else:
                    field = float(field)
                    #field = decimal.Decimal(field)
            elif fd.type == 'F':  # float
                # both numeric and float types are stored as text.
                # numeric can be decimal if numdecimal > 0
                # float is in nnn.dddddde+ppp format with numdecimal digits after decimal point.
                field = float(field)
            elif fd.type == 'C':  # character array
                # strip whitespace from text
                field = field.strip()
            elif fd.type == 'D':  # date
                # date is stored as YYYYMMDD
                if field.strip():
                    year, month, day = int(field[0:4]), int(field[4:6]), int(field[6:8])
                    field = datetime.date(year, month, day).isoformat()
                else:
                    field = None
            elif fd.type == 'L':  # boolean (logical)
                if field in 'YyTt':
                    field = True
                elif field in 'NnFf':
                    field = False
                else:
                    field = None
                    
            fields.append(field)
            pos += fd.length

        return fields

    def get_row_attribute(self, row, attr_name):
        """Returns table entry for the given row index and attribute name."""
        return self.table_rows[row][self.field_index[attr_name]]

